Skip to content

Add #[loop_match] for improved DFA codegen #138780

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 35 commits into
base: master
Choose a base branch
from

Conversation

folkertdev
Copy link
Contributor

@folkertdev folkertdev commented Mar 21, 2025

tracking issue: #132306
project goal: rust-lang/rust-project-goals#258

This PR adds the #[loop_match] attribute, which aims to improve code generation for state machines. For some (very exciting) benchmarks, see rust-lang/rust-project-goals#258 (comment)

Currently, a very restricted syntax pattern is accepted. We'd like to get feedback and merge this now before we go too far in a direction that others have concerns with.

current state

We accept code that looks like this

#[loop_match]
loop {
    state = 'blk: {
        match state {
            State::A => {
                #[const_continue]
                break 'blk State::B
            }
            State::B => { /* ... */ }
            /* ... */
        }
    }
}
  • a loop should have the same semantics with and without #[loop_match]: normal continue and break continue to work
  • #[const_continue] is only allowed in loops annotated with #[loop_match]
  • the loop body needs to have this particular shape (a single assignment to the match scrutinee, with the body a labelled block containing just a match)

future work

  • perform const evaluation on the break value
  • support more state/scrutinee types

maybe future work

  • allow continue 'label value syntax, which #[const_continue] could then use.
  • allow the match to be on an arbitrary expression (e.g. State::Initial)
  • attempt to also optimize break/continue expressions that are not marked with #[const_continue]

r? @traviscross

@rustbot rustbot added A-attributes Area: Attributes (`#[…]`, `#![…]`) S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Mar 21, 2025
@rustbot
Copy link
Collaborator

rustbot commented Mar 21, 2025

Some changes occurred in match checking

cc @Nadrieril

Some changes occurred in compiler/rustc_passes/src/check_attr.rs

cc @jdonszelmann

Some changes occurred in rustc_ty_utils::consts.rs

cc @BoxyUwU

Copy link
Contributor

@traviscross traviscross left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @folkertdev for putting up this PR. The big picture looks right, in terms of the behavior of the tests and how to approach the experiment in terms of starting with the attributes for thiis.

This is a first partial pass on the details.

@rustbot author

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Mar 22, 2025
@rust-log-analyzer

This comment has been minimized.

Copy link
Contributor Author

@folkertdev folkertdev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed review!

I've fixed a bunch of the low-hanging fruit (e.g. in the tests). For the actual pattern matching logic, I have a branch with what I believe is a better solution that re-uses more existing pattern matching infra. We'll come back to that here once björn has had a chance to look at it.

@rustbot
Copy link
Collaborator

rustbot commented Mar 24, 2025

Some changes occurred in exhaustiveness checking

cc @Nadrieril

Some changes occurred in match lowering

cc @Nadrieril

@bors
Copy link
Collaborator

bors commented Mar 26, 2025

☔ The latest upstream changes (presumably #138974) made this pull request unmergeable. Please resolve the merge conflicts.

@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@rustbot rustbot assigned Nadrieril and unassigned traviscross Apr 6, 2025
@traviscross traviscross self-assigned this Apr 6, 2025
@traviscross traviscross added the I-lang-nominated Nominated for discussion during a lang team meeting. label Apr 6, 2025
@Nadrieril
Copy link
Member

Only speaking of the MIR lowering part: my opinion on the current implementation is that this is a fine approach for an experiment, but this will need to change in depth before it can be relied on.

For one, I believe the static pattern-matching should use the const-eval interpreter instead of manually operating on valtrees. For two, it should not duplicate the work of match lowering; instead, BuiltMatchTree should track the tests cases that lead to each branch so that we can reuse them. This is the only way or-patterns can be supported properly.

Haven't reviewed the rest, would appreciate any help there, otherwise I'll get to it once approved.

@Nadrieril
Copy link
Member

In terms of the experiment, my current take is that using patterns for this doesn't pull its weight. I expect we won't allow guaranteed-direct-jump using a non-fully-const value like:

#[loop_match]
loop {
    state = 'blk: {
        match state {
            None => {
                let r = random();
                const continue 'blk Some(r);
            },
            Some(_) => break,
        }
    };
}

The reason being that this requires inspecting the expression which is a weird sort of abstraction break (you wouldn't be able to do let x = Some(r); const continue 'blk x;).

And without that, using patterns seems barely better than jump labels.

@traviscross
Copy link
Contributor

traviscross commented Apr 6, 2025

Thanks for having a look at the MIR lowering. That was indeed what I most wanted your eyes on.

For one, I believe the static pattern-matching should use the const-eval interpreter instead of manually operating on valtrees. For two, it should not duplicate the work of match lowering; instead, BuiltMatchTree should track the tests cases that lead to each branch so that we can reuse them. This is the only way or-patterns can be supported properly.

@folkertdev: What are your thoughts on this and on how and when you want to approach it?

In terms of the experiment... I expect we won't allow guaranteed-direct-jump using a non-fully-const value... The reason being that this requires inspecting the expression which is a weird sort of abstraction break (you wouldn't be able to do let x = Some(r); const continue 'blk x;).

The current implementation only supports integers and enums without fields as the scrutinee/state. Clearly we should never accept the abstraction break that you mention.

And without that, using patterns seems barely better than jump labels.

Whether or not we use patterns, what seems fairly elegant to me about this approach is that it keeps the jump labels in the value space which means that const computations can be performed to choose the jump target and that arbitrary other computations can be performed to choose the target when needed by leaving off the #[const_continue] (and accepting the codegen implications of that) without having to switch the entire block to a different syntax or engage in other workarounds.

Of course, I can imagine ways we could keep the jump labels in the value space without using patterns at all (rather than using them in restricted form by restricting the scrutinee type), and it'd be interesting to think through the pros and cons of that.

@bjorn3
Copy link
Member

bjorn3 commented Apr 7, 2025

In terms of the experiment... I expect we won't allow guaranteed-direct-jump using a non-fully-const value... The reason being that this requires inspecting the expression which is a weird sort of abstraction break (you wouldn't be able to do let x = Some(r); const continue 'blk x;).

The current implementation only supports integers and enums without fields as the scrutinee/state. Clearly we should never accept the abstraction break that you mention.

The argument of the const continue is a const value. Either a literal const {} block or for convenience directly an integer or enum literal.

For one, I believe the static pattern-matching should use the const-eval interpreter instead of manually operating on valtrees. For two, it should not duplicate the work of match lowering; instead, BuiltMatchTree should track the tests cases that lead to each branch so that we can reuse them. This is the only way or-patterns can be supported properly.

Const eval needs a fully built MIR body, but we are currently building a MIR body, so there is nothing const eval can run on. As for BuiltMatchTree, that is already used and static_pattern_match handles or patterns already. Everything is just intentionally limited to integers, bools and fieldless enums to make the implementation easier.

@Nadrieril
Copy link
Member

Nadrieril commented Apr 7, 2025

Const eval needs a fully built MIR body

I meant we should manipulate the constant with a CompileTimeInterpCx instead of going through a valtree; we indeed cannot evaluate the MIR body we're building.

As for BuiltMatchTree, that is already used and static_pattern_match handles or patterns already.

Only top-level or-patterns are handled. The specific semantics of nested or-patterns (especially mixed with guards) is tricky and not handled by static_pattern_match at all. As I've said elsewhere, using BuiltMatchTree like this (i.e. by trying to match up each or-alternative with its branch in BuiltMatchTree by relying on the order in which they show up) is not robust. I am proposing to keep the TestCases around because I expect that would make your life much easier.

I do want to be clear that I'm ok with merging this PR as is (re MIR lowering), these are only remarks for future development of the feature.

@bjorn3
Copy link
Member

bjorn3 commented Apr 7, 2025

Only top-level or-patterns are handled. The specific semantics of nested or-patterns (especially mixed with guards) is tricky and not handled by static_pattern_match at all.

Only integers, bools and fieldless enums are supported, none of which can have nested or-patterns. And guards are fundamentally incompatible with #[loop_match] as they require running arbitrary code at runtime to determine which match arm to take.

@folkertdev
Copy link
Contributor Author

For one, I believe the static pattern-matching should use the const-eval interpreter instead of manually operating on valtrees. For two, it should not duplicate the work of match lowering; instead, BuiltMatchTree should track the tests cases that lead to each branch so that we can reuse them. This is the only way or-patterns can be supported properly.

folkertdev: What are your thoughts on this and on how and when you want to approach it?

For the types that we support right now (scalars and fieldless enums), the ValTree approach seems robust, so I'd prefer to stick with that right now. It can be improved later if we all agree that CompileTimeInterpCx is better or easier.

Whether or not we use patterns, what seems fairly elegant to me about this approach is that it keeps the jump labels in the value space which means that const computations can be performed to choose the jump target and that arbitrary other computations can be performed to choose the target when needed by leaving off the #[const_continue] (and accepting the codegen implications of that) without having to switch the entire block to a different syntax or engage in other workarounds.

To add to that: having the state as a value is extremely useful for state machines that are used in streaming where the state has to be stored. It's of course possible to keep track of updating the state on every state transition manually, but having the language do it for you is convenient.

I am proposing to keep the TestCases around because I expect that would make your life much easier.

@Nadrieril are you able to work on keeping the TestCases? The pattern match code is really quite daunting for an outsider.

@folkertdev
Copy link
Contributor Author

We've processed the latest round of review comments

@rustbot ready

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Apr 8, 2025
@scottmcm
Copy link
Member

scottmcm commented Apr 9, 2025

We'd like to get feedback and merge this now before we go too far in a direction that others have concerns with.

I continue to be skeptical of the loop-match phrasing. I don't think "no new syntax" is a good argument with "well except it adds 2 new attributes and only supports limited patterns and needs custom pattern lowering", because at that point it's essentially new syntax anyway. I'm still inclined to say that this should be working on block labels that can be jumped between, making those labelled blocks directly the states in the state machine.

But none of that is experiment-blocking. I'm fine with an experiment moving forward so we can see more real examples of what you want to do with this, and less "well that's just 'a: { continue 'b; }" examples.

@traviscross
Copy link
Contributor

@rustbot labels -I-lang-nominated +I-lang-radar

We discussed this in our lang call today. We're approving this experiment. I'll continue to be the lang "champion".

To emphasize what @scottmcm said above, which he also said in discussion, he'd like to see more motivating examples for why keeping the jump labels in the value space is important. As we go forward here and work to write design meeting documents and to revise the RFC, this is something that we'll want to keep in mind and be sure to address.

@rustbot rustbot added I-lang-radar Items that are on lang's radar and will need eventual work or consideration. and removed I-lang-nominated Nominated for discussion during a lang team meeting. labels Apr 10, 2025
@folkertdev
Copy link
Contributor Author

I continue to be skeptical of the loop-match phrasing. I don't think "no new syntax" is a good argument with "well except it adds 2 new attributes and only supports limited patterns and needs custom pattern lowering", because at that point it's essentially new syntax anyway.

Kinda, yeah. The crucial difference is that it doesn't need any changes to the parser and formatter, or any tooling like rust-analyzer. So the impact is much smaller.


more motivating examples for why keeping the jump labels in the value space is important.

Sure. In short, the main motivating example here is the zlib-rs decompression state machine. It is a streaming algorithm, meaning that it must be able to pause and save its state when it runs out of input, so it can resume again when more input arrives. Beyond the rust value language being a lot more expressive than the one for labels, the ability to easily store the state is the crucial reason to us for using enums, and not labels.

The code is at https://github.com/bjorn3/zlib-rs/blob/loop_match_attr/zlib-rs/src/inflate.rs#L871, just search for const_continue to see how it's used.

As we go forward here and work to write design meeting documents and to revise the RFC, this is something that we'll want to keep in mind and be sure to address.

Right. With this PR more or less complete, maybe we should strategize a bit what good concrete next steps are?

@Nadrieril
Copy link
Member

Only integers, bools and fieldless enums are supported

Does the feature not intend to go further than that? Again, I'm just saying "btw, here are my ideas for implementing the more complete version of this". Current state is ok for the experiment.

@Nadrieril are you able to work on keeping the TestCases?

Will do, I had plans along these lines already :) No promises on the delay tho.

@folkertdev
Copy link
Contributor Author

Does the feature not intend to go further than that? Again, I'm just saying "btw, here are my ideas for implementing the more complete version of this". Current state is ok for the experiment.

We'll cover all types eventually, so we'll keep that in mind.

Will do, I had plans along these lines already :) No promises on the delay tho.

Sounds good! I'd be happy to do actual implementation work too, but in terms of design you'll have a much better idea of how the pieces do/should fit together.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-attributes Area: Attributes (`#[…]`, `#![…]`) I-lang-radar Items that are on lang's radar and will need eventual work or consideration. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants